ImageView
ImageView 的核心功能是显示 Drawable,因此其核心方法是
1
| public void setImageDrawable(@Nullable Drawable drawable)
|
传入新的 Drawable 对象后,会配置其各种属性,包括 level,state,染色,Bounds等。
onDraw 方法就是对 Drawable 对象的绘制,但有两点要注意
- Matrix 通过左乘对 canvas 产生影响 ,可以用于图片处理
- mCropToPadding 通过 canvas 的 clipRect 方法将对显示区域做截取, 并将 padding 纳入 Drawable 的Bound计算
1 2 3 4 5 6 7 8 9 10
| canvas.save(); if (mCropToPadding) { canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, scrollX + mRight - mLeft - mPaddingRight, scrollY + mBottom - mTop - mPaddingBottom); } canvas.translate(mPaddingLeft, mPaddingTop); canvas.concat(mDrawMatrix); mDrawable.draw(canvas); canvas.restoreToCount(saveCount);
|
如果设置了 mCropToPadding 为 true,则滚动ImageView
子类 ImageButton 虽然名为 “Button”,却不是 TextView,而是 ImageView,只不过它用了与 Button 相同的背景
1
| <item name="background">@drawable/btn_default</item>
|
子类 FloatingActionButton 的特别之处在于它强制定义了背景 Drawable,其默认配置如下
1 2 3 4 5 6 7 8 9
| <style name="Widget.Design.FloatingActionButton" parent="android:Widget"> <item name="android:background">@drawable/design_fab_background</item> //白色的圆形 shapedrawable <item name="backgroundTint">?attr/colorAccent</item> // 背景的渲染色是 colorAccent <item name="fabSize">normal</item> <item name="elevation">@dimen/design_fab_elevation</item> // 6dp <item name="pressedTranslationZ">@dimen/design_fab_translation_z_pressed</item> // 6dp <item name="rippleColor">?attr/colorControlHighlight</item> <item name="borderWidth">@dimen/design_fab_border_width</item> // 0.5dp </style>
|
然而实际的背景并不是简单地 ShapeDrawable,还要考虑描边和ripple的效果,其实现在不同的版本各不相同。
绘制形状的改造
圆形控件:CircleImageView库实现了圆形图片,实际是重新实现了 ImageView 的绘制方法,它的绘制原理是从 Drawable 中提取出位图 Bitmap 对象,而后使用其作为 BitmapShader 的像素源,绘制圆形图片。
它的具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override protected void onDraw(Canvas canvas) { Drawable drawable = getDrawable(); Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), ARGB_8888); Canvas c = new Canvas(bitmap); drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); drawable.draw(c); BitmapShader bitmapShader = new BitmapShader(bitmap, CLAMP, CLAMP); Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); bitmapPaint.setShader(bitmapShader); int width = getWidth() - getPaddingLeft() - getPaddingRight(); int height = getHeight() - getPaddingTop() - getPaddingBottom(); int radius = Math.min(width / 2, height / 2); int centerX = getPaddingLeft() + width / 2; int centerY = getPaddingTop() + height / 2; canvas.drawCircle(centerX, centerY, radius, bitmapPaint); }
|
实际上这也可以通过 canvas 的 clipPath 方法来实现,但截取操作在底层开销比较大,宜使用 Shader 方法。
v4 包中的 CircleImageView 专门用作下拉刷新控件,它的背景 Drawable 设置为圆形的 ShapeDrawable,内容 Drawable 设置为带动画效果的箭头 Drawable。
RoundedImageView库实现圆角图片,其实现原理与上面的 CircleImageView 类似,以(0,0,200,200)为区域,半径为 20 的圆角矩形为例,最终实现方法是
1 2 3 4 5 6 7
| @Override public void draw(@NonNull Canvas canvas) { BitmapShader bitmapShader = new BitmapShader(mBitmap, mTileModeX, mTileModeY); canvas.drawRoundRect(mDrawableRect, radius, radius, mBitmapPaint); redrawBitmapForSquareCorners(canvas); }
|
只是绘制由圆形变成了圆角矩形而已,但是如何只绘制单个原角呢?这里的做法是重绘,例如左上角,重绘的区域即(0,0,20,20)的矩形,将这部分的位图重绘出来就行了。如果是边界,则重绘线段。
这里的问题是发生了重绘,在有三个圆角的情况下最为糟糕,因此应该避免这种情况。如果自己实现,可以将矩形区域划分的细一些,以便一次绘制完毕。也可以采用 Shape 的做法,通过构建路径的方式来实现。
绘制内容的添加
绘制内容的添加即在 ImageView 之上进行扩展绘制。
SlantedTextView库的效果可以采用额外绘制的方法实现。
SwitchIcon库的效果如下
该库所做的额外绘制稍显复杂,包括
1.斜线是通过 Paint 绘制 Line 来实现的,其起始点在
1 2
| dashXStart = getPaddingLeft() + 0.5f * SIN_45 * dashThickness; dashYStart = getPaddingTop() + 1.5f * SIN_45 * dashThickness;
|
结束点在
1 2
| dashEnd.x = (int) (dashXStart + width - delta1); dashEnd.y = (int) (dashYStart + height - delta2);
|
这样斜线就是从左上向右下逐渐延伸的。
2.斜线的延伸由参数 friction 控制,除此之外,friction 还控制渲染的颜色,透明度的变化。这里原本的 drawable 变色是通过构建新的 PorterDuffColorFilter 来完成的,而斜线的颜色是通过Paint来设置的。
3.这里还有最后一个问题:就是斜线和原 Drawable 的重叠,其解决方法如下
1 2 3 4 5
| protected void onDraw(Canvas canvas) { drawDash(canvas); canvas.clipPath(clipPath, Region.Op.XOR); super.onDraw(canvas); }
|
这里 clipPath 覆盖斜线,并略微大于斜线,值得注意的是区域截取的方式采用的是 XOR,这保证了Canvas 将在斜线区域之外绘制原 Drawable。
动画效果
AndroidScrollingImageView这种效果实际是视差造成的,动的是背景图,实际绘制的是多个 Bitmap,通过设置 offset 参数造成偏移效果,并通过控制此值形成动画效果。最终效果实际上与背景素材有关,不同的素材可以设置不同的回退速度。
KenBurnsView 库实现 Ken Burns 效果,即景深效果,
首先确定控件的尺寸和Bitmap尺寸是不一致的,后者要大于前者。在 Bitmap 尺寸范围内截取一个空间尺寸大小的区域,同时显示区域用动画移位过去,就是 Ken Burns 效果。
效果的实现与位置形状矩阵 Matrix 有关,这里需要将ScaleType类型设置为MATRIX
1
| super.setScaleType(ImageView.ScaleType.MATRIX);
|
这里移位采用 Matrix 来完成,动画由 mProcess 参数来控制。
图像处理
图像处理主要依赖于颜色矩阵(ColorMatrix)来实现。
ColorMatrix
ColorMatrix 是一个 4*5 的矩阵,4行分别代表red,green,blue和alpha向量,默认是单位阵。在实现上采用的是float数组来存储这些数据。
1 2 3
| public class ColorMatrix { private final float[] mArray = new float[20]; }
|
最简单的操作像素颜色变化的方法是 setScale,它只改变了对角线的上的数据,这样颜色的各个分量独立的进行变化
1 2 3 4 5 6 7 8 9 10
| public void setScale(float rScale, float gScale, float bScale, float aScale) { final float[] a = mArray; for (int i = 19; i > 0; --i) { a[i] = 0; } a[0] = rScale; a[6] = gScale; a[12] = bScale; a[18] = aScale; }
|
图像Bitmap由像素构成,像素又包括对比度(Contrast),亮度(Brightness),纯度(saturation)等参数,图片的重叠还涉及混合模式(Mode)。修改这些信息主要通过颜色矩阵(ColorMatrix)来完成。
关于构建 marix 的方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| private static float[] calculateMatrix(int mode, int brightness, float contrast, float saturation) { return applyBrightnessAndContrast(getMatrixByMode(mode, saturation), brightness, contrast); } private static float[] applyBrightnessAndContrast(float[] matrix, int brightness, float contrast) { float t = (1.0F - contrast) / 2.0F * 255.0F; for (int i = 0; i < 3; i++) { for (int j = i * 5; j < i * 5 + 3; j++) { matrix[j] *= contrast; } matrix[5 * i + 4] += t + brightness; } return matrix; }
|
正常情况下直接改变像素颜色
1 2
| final float[] matrix = calculateMatrix(mode, brightness, contrast, saturation); drawableHolder.getDrawable().setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(matrix)));
|
如果要在改变时形成动画,则需要利用颜色矩阵中的float数组作为起始值,并利用值动画来更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public void updateStyle() { final float[] matrix = calculateMatrix(mode, brightness, contrast, saturation); if (enableAnimation) { animateMatrix(oldMatrix, matrix, new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); setDrawableStyleByMatrix(matrix); } }); } } private void animateMatrix(final float[] startMatrix, final float[] endMatrix, AnimatorListenerAdapter onAnimationEndListener) { animator = ValueAnimator.ofFloat(0F, 1F).setDuration(animationDuration); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float[] result = new float[20]; float fraction = valueAnimator.getAnimatedFraction(); float progress = interpolator.getInterpolation(fraction); for (int i = 0; i < 20; i++) { result[i] = (startMatrix[i] * (1 - progress)) + (endMatrix[i] * progress); } drawableHolder.getDrawable().setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(matrix))); } }); animator.addListener(onAnimationEndListener); animator.start(); }
|